package com.ecnu.sei.app;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;

import org.apache.log4j.PropertyConfigurator;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ecnu.sei.app.course.AbstractCourse;
import com.ecnu.sei.app.course.CompulsoryCourse;
import com.ecnu.sei.app.course.SpecialtyElective;
import com.ecnu.sei.app.resources.Classroom;
import com.ecnu.sei.app.resources.Time;
import com.ecnu.sei.app.teacher.AbstractTeacher;
import com.ecnu.sei.app.teacher.Official;
import com.ecnu.sei.app.util.Pair;
import com.ecnu.sei.app.util.Utils.CollegeYear;
import com.ecnu.sei.app.util.Utils.DayHour;

/*
 * in memory containers to keep free slots and detect collisions
 */

public class Scheduler {

  private static Scheduler instance = null;

  private static final Logger LOG = LoggerFactory.getLogger(Scheduler.class);

  private Set<CompulsoryCourse> compulsoryCourses = new HashSet<CompulsoryCourse>();

  private Set<SpecialtyElective> electiveCourses = new HashSet<SpecialtyElective>();

 // private List<AbstractTeacher> teachers = new ArrayList<AbstractTeacher>(); 

  private Set<AbstractTeacher> teachers = new HashSet<AbstractTeacher>();
  
  private List<AbstractTeacher> tiredTeachers = new ArrayList<AbstractTeacher>();

  private List<Official> officials = new ArrayList<Official>();

  // list of backup proctors in case that one takes a leave 
  private List<AbstractTeacher> backups = new ArrayList<AbstractTeacher>();

  // free resources for compulsory courses
  private Map<DayHour, Map<Time, Set<Classroom>>> free = new HashMap<DayHour, Map<Time, Set<Classroom>>>();

  // default schedule for compulsory courses
  private Map<CollegeYear, DayHour> defaultSchedule = new HashMap<CollegeYear, DayHour>();

  // exam schedule of a college year
  private Map<CollegeYear, Set<Time>> cySchedule = new HashMap<CollegeYear, Set<Time>>();


  private Map<Time, Set<AbstractCourse>> examSchedule = new HashMap<Time, Set<AbstractCourse>>();


  private Time electiveStartTime = null;
  private Time electiveEndTime = null;
  private Time compulsoryStartTime = null;
  private Time compulsoryEndTime = null; 


  protected final Classroom[] classroomList = {
      new Classroom("新一"),
      new Classroom("新二"), 
      new Classroom("新三"), 
      new Classroom("新四") 
  };

  static {
	  File directory = new File("");//设定为当前文件夹  
	  try{  
	      System.out.println(directory.getCanonicalPath());//获取标准的路径  
	      System.out.println(directory.getAbsolutePath());//获取绝对路径  
	  }catch(Exception e){}  
	  
    PropertyConfigurator.configure("conf/log4j.properties");
    instance = new Scheduler(); // singleton
    LOG.info("scheduler inited");

  }

  private Scheduler() {
    defaultSchedule.put(CollegeYear.FRESHMAN, DayHour.AFTN_TWO);
    defaultSchedule.put(CollegeYear.SEPHOMORE, DayHour.AFTN_ONE);
    defaultSchedule.put(CollegeYear.JUNIOR, DayHour.MORN_TWO);
    defaultSchedule.put(CollegeYear.SENIOR, DayHour.MORN_ONE);

  }

  public void clear() {
    compulsoryCourses.clear();
    electiveCourses.clear();
    teachers.clear();
    tiredTeachers.clear();
    officials.clear();
    backups.clear();
    free.clear();
    cySchedule.clear();
    examSchedule.clear();
  }
  
  public static Scheduler getScheduler() {
    return instance;
  }


  // the main logic
  public void generateSchedule() {
    generateCompulsory();
    generateElective();
  }

  protected void generateCompulsory() {
    LOG.info("loading free resource for compulsory courses");
    loadFreeResource(compulsoryStartTime, compulsoryEndTime, classroomList);
    Set<AbstractCourse> courses = new HashSet<AbstractCourse>(compulsoryCourses);
    generateSchedule(courses);
  }

  protected void generateElective() {
    LOG.info("loading free resource for elective courses");
    loadFreeResource(electiveStartTime, electiveEndTime, null);
    Set<AbstractCourse> courses = new HashSet<AbstractCourse>(electiveCourses);
    generateSchedule(courses);

  }

  private void generateSchedule(Set<AbstractCourse> courses) {

    LOG.info(teachers.size() + " teachers");
    LOG.info(courses.size() + " courses");

    // courses that could not get resources or proctors after first run

    Set<AbstractCourse> pending = new HashSet<AbstractCourse>();
    Set<AbstractCourse> leftout = new HashSet<AbstractCourse>();
    Set<AbstractCourse> eligible = new HashSet<AbstractCourse>();
    eligible.addAll(courses);

    // we firstly set resource according to default schedule
    LOG.info("set time and classroom, for all");
    for (AbstractCourse course : eligible) {
      if (!course.setResource()) {
        pending.add(course);
      } 
    }

    LOG.info(pending.size() + " pendings");
    LOG.info("set time and classroom, for pending courses");
    for (Iterator<AbstractCourse> iterator = pending.iterator(); iterator.hasNext(); ) {
      AbstractCourse course = iterator.next();
      if (course.setResourcePending()) {
        iterator.remove();
      }  else {
        leftout.add(course);
        eligible.remove(course);
      }
    }

    pending.clear(); 
    LOG.info("set proctors, firstly course teachers");
    for (AbstractCourse course : eligible) {
      AbstractTeacher teacher = course.getCourseInfo().getTeacher(); 
      if (course.addProctor(teacher)) {
        teacher.addProctor(course);
        if (teacher.isExceedLimit()) {
          tiredTeachers.add(teacher);
        }
      }
      if (!course.hasEnoughProctors()) {
        pending.add(course);
      }
    }

    LOG.info("set proctors, add rest proctors"); 
    int index = 0;
    int loopNum = 0;
    while (!pending.isEmpty()) {
      if (tiredTeachers.size() >= teachers.size()) {
        LOG.info("every teacher has reached proctoring limit");
        break;
      }
      if (loopNum >= 1000) {
        LOG.info("reach max loop counts, break;");
        break;
      }
      Object[] teacherArray = teachers.toArray();
      for (Iterator<AbstractCourse> iterator = pending.iterator(); iterator.hasNext(); ) {
        AbstractCourse course = iterator.next();
        int last = index;
        boolean find = true;
       
       // while (!course.addProctor(teachers.get(index))) {
       while (!course.addProctor((AbstractTeacher) teacherArray[index])) {
         // index = index == (teachers.size() - 1) ? 0 : index + 1;
          index = index == (teacherArray.length - 1) ? 0 : index + 1;
          if (index == last) {
            find = false;
            break;
          }
        }

        if (find) {
         // AbstractTeacher teacher = teachers.get(index);
          AbstractTeacher teacher = (AbstractTeacher) teacherArray[index];
          teacher.addProctor(course);
          if (teacher.isExceedLimit()) {
            tiredTeachers.add(teacher);
          }
        }

        if (course.hasEnoughProctors() && pending.contains(course)) {
          iterator.remove();
        }

        index = index == (teachers.size() - 1) ? 0 : index + 1;
      }
      loopNum++;

    }

    eligible.removeAll(pending);
    leftout.addAll(pending);
    pending.clear();
    // arrange for patrols
    LOG.info("add patrols");
    int patrolIndex = 0;
    for (Iterator<AbstractCourse> iterator = eligible.iterator(); iterator.hasNext(); ) {
      int last = patrolIndex;
      AbstractCourse course = iterator.next();
      while (!course.setPatrol(officials.get(patrolIndex))) {
        patrolIndex = patrolIndex >= officials.size() - 1 ? 0 : patrolIndex + 1;
        if (patrolIndex == last) {
          iterator.remove();
          leftout.add(course);
          break;
        }
      }
    }

    LOG.info("add backups");
    addBackups();
    LOG.info(backups.size() + " backups");
    int backupIndex = 0;
    for (Iterator<AbstractCourse> iterator = eligible.iterator(); iterator.hasNext(); ) {
      int last = backupIndex;
      AbstractCourse course = iterator.next();
      while (!course.setBackup(backups.get(backupIndex))) {
        backupIndex = backupIndex >= backups.size() - 1 ? 0 : backupIndex + 1;
        if (backupIndex == last) {
     /*     iterator.remove();
          leftout.add(course); */
          break;
        }
      }
    }

    if (leftout.size() > 0) {
      StringBuilder sb = new StringBuilder();
      sb.append("the following courses cannot be arranged for exams:\n");    
      for (AbstractCourse course : leftout) {
          sb.append(course.getCourseInfo().getCourseName() + "\n");
      }
      LOG.warn(sb.toString());    
    }
    
    for (AbstractCourse course: eligible) {
      addExamSchedule(course);
    }
  }

  private void loadFreeResource(Time startTime, Time endTime, Classroom[] classrooms) {
    if (null == startTime || null == endTime
        || startTime.compareTo(endTime) > 0) {
      LOG.debug("exam time error");
      return;
    } 

    // elective courses
    if (null == classrooms) {
      return;
    }

    Time time = startTime;
    while (time.compareTo(endTime) < 0) {
      for (DayHour dayHour : DayHour.getList()) {
        time.setDayHour(dayHour);
        addFreeResource(new Time(time), classrooms); 
      }
      time.addDays(1);
      time.setDayHour(DayHour.MORN_ONE);
    }
  }

  // load courses and teachers
  public void addCourse(AbstractCourse course) {
    if (course instanceof CompulsoryCourse) {
      compulsoryCourses.add((CompulsoryCourse) course);
    } else if (course instanceof SpecialtyElective) {
      electiveCourses.add((SpecialtyElective) course);
    }
  }

  public void addCourseList(List<AbstractCourse> courseList) {
    for (AbstractCourse course : courseList) {
      addCourse(course);
    }
  }

  public void addTeacher(AbstractTeacher teacher) {
    teachers.add(teacher);
  
    if (teacher instanceof Official) {
      officials.add((Official) teacher);
    }
  }

  public void addTeacherList(List<AbstractTeacher> teacherList) {
    for(AbstractTeacher teacher : teacherList) {
      addTeacher(teacher);
    }
  }

  public void addFreeResource(Time time, Classroom[] classroomList) {
    for (int i = 0; i < classroomList.length; i++) {
      addFreeResource(time, classroomList[i]);
    }

  }

  private void addFreeResource(Time time, Classroom classroom) {
    DayHour dayHour = time.getDayHour();
    if (null == free.get(dayHour)) {
      Map<Time, Set<Classroom>> resourceMap = new HashMap<Time, Set<Classroom>>();
      free.put(dayHour, resourceMap);
    } 
    if (null == free.get(dayHour).get(time)) {
      free.get(dayHour).put(time, new HashSet<Classroom>());
    }
    free.get(dayHour).get(time).add(classroom);
  }

  private void addExamSchedule(AbstractCourse course) {
    Time time = course.getExamTime();
    if (null == examSchedule.get(time)) {
      examSchedule.put(time, new HashSet<AbstractCourse>());
    }
    examSchedule.get(time).add(course);
  }

  public Map<Time, Set<AbstractCourse>> getExamSchedule() {
    return examSchedule;
  }
  
  private boolean noFreeResource() {
    if (null == free || 0 == free.size()) {
      return true;
    } else {
      return false;
    }
  }



  // teachers whose proctor counts are below average
  private void addBackups() {
    int sum = 0;
    for (AbstractTeacher teacher : teachers) {
      sum += teacher.getCount();
    }
    float avg = ((float)sum) / teachers.size();

    for (AbstractTeacher teacher : teachers) {
      if (teacher.getCount() < avg) {
        backups.add(teacher);
      }
    }
  }


  private Map<Time, Set<Classroom>> getFreeByDayHour(DayHour dayHour) {
    return free.get(dayHour);
  }

  private Map<Time, Set<Classroom>> getFreeByTime(Time time) {
    return getFreeByDayHour(time.getDayHour());
  }

  public Pair<Time, Classroom> getFreeResource(CollegeYear collegeYear) {
    DayHour dayHour = defaultDayHourForCollegeYear(collegeYear);
    System.out.print("day hour:"+free.toString());

    Map<Time, Set<Classroom>> freeResource = getFreeByDayHour(dayHour);
    for (Time time : freeResource.keySet()) {

      // no continuous exams for one college year at one morning or afternoon
      if (hasExamSameHalfDay(collegeYear, time)) {
        continue;
      }

      Set<Classroom> freeClassrooms = freeResource.get(time);
      if (freeClassrooms != null && !freeClassrooms.isEmpty()) {
        Iterator<Classroom> iterator = freeClassrooms.iterator();
        if (iterator.hasNext()) {
          Classroom classroom = iterator.next();
          updateCYSchedule(collegeYear, time);
          updateFree(time, classroom);
          return new Pair<Time, Classroom>(time, classroom);
        }
      }
    }

    return null;
  }

  private boolean hasExamSameHalfDay(CollegeYear collegeYear, Time time) {
    if (null == cySchedule || cySchedule.size() == 0) {
      return false;
    }
    for (Time other : cySchedule.get(collegeYear)) {
      if (null != other && time.isSameHalfDay(other)) {
        return true;
      }
    }
    return false;
  }

  private void updateCYSchedule(CollegeYear collegeYear, Time time) {
    if (cySchedule.containsKey(collegeYear)) {
      cySchedule.get(collegeYear).add(time);
    }
  }

  private void updateFree(Time time, Classroom classroom) {
    DayHour dayHour = time.getDayHour();

    Map<Time, Set<Classroom>> resourceMap = free.get(dayHour);

    if (resourceMap != null && !resourceMap.isEmpty()) {
      Set<Classroom> classroomSet = resourceMap.get(time);
      if (classroomSet != null && !classroomSet.isEmpty()) {
        classroomSet.remove(classroom);
      }
      if (classroomSet.isEmpty()) {
        resourceMap.remove(time);
      }
    }
  }

  public Map<CollegeYear, DayHour> getDefaultSchedule() {
    return defaultSchedule;
  }

  public DayHour defaultDayHourForCollegeYear(CollegeYear collegeYear) {
    return defaultSchedule.get(collegeYear);
  }


  public Time getElectiveStartTime() {
    return electiveStartTime;
  }

  public Time getCompulsoryStartTime() {
    return compulsoryStartTime;
  }

  public void setElectiveStartTime(Time electiveStartTime) {
    electiveStartTime.setDayHour(DayHour.MORN_ONE);
    this.electiveStartTime = electiveStartTime;
  }

  public void setCompulsoryStartTime(Time compulsoryStartTime) {
    compulsoryStartTime.setDayHour(DayHour.MORN_ONE);
    this.compulsoryStartTime = compulsoryStartTime;
  }

  public Time getElectiveEndTime() {
    return electiveEndTime;
  }

  public Time getCompulsoryEndTime() {
    return compulsoryEndTime;
  }

  public void setElectiveEndTime(Time electiveEndTime) {
    electiveEndTime.setDayHour(DayHour.NONE);
    this.electiveEndTime = electiveEndTime;
  }

  public void setCompulsoryEndTime(Time compulsoryEndTime) {
    compulsoryEndTime.setDayHour(DayHour.NONE);
    this.compulsoryEndTime = compulsoryEndTime;
  }

  public Set<CompulsoryCourse> getCompulsoryCourses() {
    return compulsoryCourses;
  }

  public Set<SpecialtyElective> getElectiveCourses() {
    return electiveCourses;
  }

}
